package com.github.cakin.shiro.chapter18.oauth2;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @className: OAuth2AuthenticationFilter
 * @description: 该 filter 的作用类似于 FormAuthenticationFilter 用于 oauth2 客户端的身份验证控制；
 * 如果当前用户还没有身份验证，首先会判断 url 中是否有 code（服务端返回的 auth code），
 * 如果没有则重定向到服务端进行登录并授权，然后返回 auth code；
 * 接着 OAuth2AuthenticationFilter 会用 auth code 创建 OAuth2Token，
 * 然后提交给 Subject.login 进行登录；接着 OAuth2Realm 会根据 OAuth2Token 进行相应的登录逻辑。
 * @date: 2020/5/27
 * @author: cakin
 */
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {

    /**
     * oauth2 authc code参数名
     */
    private String authcCodeParam = "code";
    /**
     * 客户端id
     */
    private String clientId;
    /**
     * 服务器端登录成功/失败后重定向到的客户端地址
     */
    private String redirectUrl;
    /**
     * oauth2服务器响应类型
     */
    private String responseType = "code";

    /**
     * 失败Url
     */
    private String failureUrl;

    public void setAuthcCodeParam( String authcCodeParam ) {
        this.authcCodeParam = authcCodeParam;
    }

    public void setClientId( String clientId ) {
        this.clientId = clientId;
    }

    public void setRedirectUrl( String redirectUrl ) {
        this.redirectUrl = redirectUrl;
    }

    public void setResponseType( String responseType ) {
        this.responseType = responseType;
    }

    public void setFailureUrl( String failureUrl ) {
        this.failureUrl = failureUrl;
    }

    /**
     * 功能描述：通过 auth code 创建 OAuth2Token 提交给 Subject 进行登录
     *
     * @param request 请求
     * @param response 响应
     * @return AuthenticationToken 认证token
     * @author cakin
     * @date 2020/5/27
     * @description: http://localhost:9080/chapter17-client/oauth2-login?code=b1b8fa868c502d69e0760c4c0aea8483
     * 这个地址是服务端的重定向地址，在此获取其中的授权码，并创建token
     */
    @Override
    protected AuthenticationToken createToken( ServletRequest request, ServletResponse response ) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String code = httpRequest.getParameter(authcCodeParam);
        OAuth2Token oAuth2Token = new OAuth2Token(code);
        return oAuth2Token;
    }

    @Override
    protected boolean isAccessAllowed( ServletRequest request, ServletResponse response, Object mappedValue ) {
        return false;
    }

    /**
     * 功能描述：登录认证
     *
     * @param request 请求
     * @param response 响应
     * @return boolean
     * @author cakin
     * @date 2020/5/27
     * @description: 该函数会进两次
     * 第一次：客户端请求在此拦截 http://localhost:9080/chapter17-client/oauth2-login
     * 第二次：服务端的重定向地址：http://localhost:9080/chapter17-client/oauth2-login?code=b1b8fa868c502d69e0760c4c0aea8483
     * 在此拦截
     */
    @Override
    protected boolean onAccessDenied( ServletRequest request, ServletResponse response ) throws Exception {
        String error = request.getParameter("error");
        String errorDescription = request.getParameter("error_description");
        // 判断有没有服务端返回的 error 参数，如果有则直接重定向到失败页面
        if (!StringUtils.isEmpty(error)) {
            WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);
            return false;
        }

        Subject subject = getSubject(request, response);
        // 如果用户还没有身份验证，判断是否有 auth code 参数（即是不是服务端授权之后返回的），如果没有则重定向到服务端进行授权
        if (!subject.isAuthenticated()) {
            if (StringUtils.isEmpty(request.getParameter(authcCodeParam))) {
                // 如果用户没有身份验证，且没有auth code，则重定向到服务端授权
                // http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee
                // &response_type=code
                // &redirect_uri=http://localhost:9080/chapter17-client/oauth2-login
                saveRequestAndRedirectToLogin(request, response);
                return false;
            }
        }
        // 调用 executeLogin 进行登录，接着 OAuth2Realm 会根据 OAuth2Token 进行相应的登录逻辑。
        return executeLogin(request, response);
    }

    /**
     * 功能描述：登录成功将回调 onLoginSuccess 方法重定向到成功页面
     *
     * @param token    token
     * @param subject  主体
     * @param request  请求
     * @param response 响应
     * @return boolean
     * @author cakin
     * @date 2020/5/27
     */
    @Override
    protected boolean onLoginSuccess( AuthenticationToken token, Subject subject, ServletRequest request,
                                      ServletResponse response ) throws Exception {
        issueSuccessRedirect(request, response);
        return false;
    }

    /**
     * 功能描述：登录失败则回调 onLoginFailure 重定向到失败页面。
     *
     * @param token    token
     * @param ae       异常
     * @param request  请求
     * @param response 响应
     * @return boolean
     * @author cakin
     * @date 2020/5/27
     */
    @Override
    protected boolean onLoginFailure( AuthenticationToken token, AuthenticationException ae, ServletRequest request,
                                      ServletResponse response ) {
        Subject subject = getSubject(request, response);
        if (subject.isAuthenticated() || subject.isRemembered()) {
            try {
                issueSuccessRedirect(request, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            try {
                WebUtils.issueRedirect(request, response, failureUrl);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

}
